package com.github.shell88.bddvideoannotator.service; import com.github.shell88.bddvideoannotator.annotationfile.exporter.AnnotationExporter; import com.github.shell88.bddvideoannotator.annotationfile.exporter.EafAnnotationExporter; import com.github.shell88.bddvideoannotator.annotationfile.exporter.Helper; import com.github.shell88.bddvideoannotator.annotationfile.exporter.ScenarioAnnotationsDto; import com.github.shell88.bddvideoannotator.annotationfile.exporter.StepAnnotation; import com.github.shell88.bddvideoannotator.annotationfile.exporter.StepResult; import com.github.shell88.bddvideoannotator.videorecorder.MonteVideoRecorderAdapter; import com.github.shell88.bddvideoannotator.videorecorder.VideoRecorder; import java.awt.Dimension; import java.awt.Toolkit; import java.io.File; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebService; import javax.jws.soap.SOAPBinding; import javax.xml.ws.Endpoint; import javax.xml.ws.WebServiceException; /** * Main Class for starting the soap-based annotation service. * * @author Hell */ @WebService(name = "AnnotationService") @SOAPBinding(style = SOAPBinding.Style.RPC) public class AnnotationService { private AnnotationExporter annotationExporter; private ScenarioAnnotationsDto currentScenario = new ScenarioAnnotationsDto(); /** * The systemTimestamp when the scenario was started using * {@link #startScenario(String)}. */ private Long scenarioStartTimestamp; /** The position of the last added stepResult. */ private int resultPos; /** * Represents the end timestamp of the last result {@link #resultPos}. */ private Long currentEndTimestamp; /** * Description of the currentScenario {@link #currentScenarioName}. */ /** Path to the videoOutputFile for referencing in the annotationFile. */ private File videoOutputFile = null; /** Videorecorder used for recording the screencast. */ private VideoRecorder videoRecorder; /** Capturing Area for the Screencast. */ private int videoHeight; /** Capturing Area for the Screencast. */ private int videoWidth; /** Directory where to store the video and annotation outputFile. */ private File outputDirectory; /** * Necessary for generating java client. DO NOT USE. */ public AnnotationService() { } /** * Initalizes a new annotation-Service. * * @param path * {@link #outputDirectory} * @param capturingWidth * {@link #videoWidth} * @param capturingHeight * {@link #videoHeight} */ public AnnotationService(String path, String capturingWidth, String capturingHeight) { if (capturingWidth.equalsIgnoreCase("full")) { videoWidth = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth(); } else { videoWidth = Integer.parseInt(capturingWidth); if (videoWidth < 0 || videoWidth > (int) Toolkit.getDefaultToolkit().getScreenSize() .getWidth()) { throw new IllegalArgumentException( "Video width has illegal dimension: " + videoWidth); } } if (capturingHeight.equalsIgnoreCase("full")) { videoHeight = (int) Toolkit.getDefaultToolkit().getScreenSize() .getHeight(); } else { videoHeight = Integer.parseInt(capturingHeight); if (videoHeight < 0 || videoHeight > (int) Toolkit.getDefaultToolkit().getScreenSize() .getWidth()) { throw new IllegalArgumentException( "Video height has illegal dimension: " + videoHeight); } } changeOutputDirectory(path); } @WebMethod(operationName = "setFeatureText") public void setFeatureText(@WebParam(name = "featureText") String featureText) { currentScenario.setFeatureText(featureText); } /** * Starts a new annotation file/video file. For each Scenario an own * annotation file will be generated. Also Scenario Outlines will be collected * to one Annotation File. * * @param scenarioName * The description for the started scenario. It will be used for * naming the output annotation file/video file. */ @WebMethod(operationName = "startScenario") public void startScenario(@WebParam(name = "scenarioName") String scenarioName) { if (!currentScenario.hasStepAnnotations()) { resultPos = 0; currentEndTimestamp = scenarioStartTimestamp = System.currentTimeMillis(); } currentScenario.setScenarioText(scenarioName); startVideoRecording(); } /** * Stops the current Scenario and writes the appropriate output files. */ public void stopScenario() { this.stopVideoRecording(); for (int i = resultPos; i < currentScenario.getNumberOfStepAnnotations(); i++) { currentScenario.getStepAnnotation(resultPos).setMillisecondsFrom(currentEndTimestamp); } this.writeAnnotationFile(); currentScenario = new ScenarioAnnotationsDto(); } /** * Writes the buffered Annotations to the annotation output file and stops * video recording. */ private void writeAnnotationFile() { if (!currentScenario.hasStepAnnotations()) { return; } currentScenario.setNameVideoFile(videoOutputFile.getName()); try { currentScenario.setSha1ChecksumVideo(Helper.calcSha1Checksum(videoOutputFile)); annotationExporter.write(currentScenario); } catch (Exception e) { throw new WebServiceException("Could not write Annotation-Outputfile: " + e.getMessage()); } finally { currentScenario = null; } } /** * Can be used to change the outputDirectory for the annotation files/video * files at runtime. * * @param path * The new target outputDirectory */ @WebMethod(operationName = "changeOutputDirectory") public void changeOutputDirectory(@WebParam(name = "path") String path) { File changedOutputDirectory = new File(path); if (changedOutputDirectory.isFile()) { throw new IllegalArgumentException(changedOutputDirectory.toString() + " is a file!"); } if (!changedOutputDirectory.exists()) { if (!changedOutputDirectory.mkdirs()) { throw new IllegalArgumentException("Could not create OutputDirectory: " + changedOutputDirectory.toString()); } } this.outputDirectory = changedOutputDirectory; this.annotationExporter = new EafAnnotationExporter(outputDirectory); } /** * Starts a screencast. */ private void startVideoRecording() { if (videoRecorder != null) { return; } String prefix = "screencast"; if (currentScenario.getScenarioText() != "") { prefix = currentScenario.getScenarioText(); } File outputfile = Helper .createNewOutputFile(outputDirectory, prefix, "avi"); Dimension dim = new Dimension(videoWidth, videoHeight); try { videoRecorder = new MonteVideoRecorderAdapter(outputfile, dim); videoRecorder.startVideoRecording(); } catch (Exception e) { throw new WebServiceException("Could not start videorecording: " + e.getMessage()); } } /** * Stops a screencast. */ private void stopVideoRecording() { if (videoRecorder == null) { return; } try { videoRecorder.stopVideoRecording(); videoOutputFile = videoRecorder.getOutputFile(); } catch (Exception e) { throw new WebServiceException("Could not stop videorecording: " + e.getMessage()); } finally { videoRecorder = null; } } /** * Adds a steptext to a buffer. For each step a result will be sent later. See * also {@link #addResultToBufferStep(StepResult)}. * * @param steptext * The gherkin text for the step * @param datatable * Optional input data for the step */ @WebMethod(operationName = "addStepToBuffer") public void addStepToBuffer(@WebParam(name = "steptext") String steptext, @WebParam(name = "datatable") String[][] datatable) { if (currentScenario == null) { // Scenario not started => return => no adding necessary return; } StepAnnotation stepAnnot = new StepAnnotation(); stepAnnot.setSteptext(steptext); stepAnnot.setDataTables(datatable); /* * If JVM terminates unexpected, result "Error" will be written by the * shutdown hook of the adapter */ stepAnnot.setStepResult(StepResult.ERROR); stepAnnot.setDurationMillis(0L); stepAnnot.setMillisecondsFrom(0L); currentScenario.addStepAnnotation(stepAnnot); } /** * Adds a result to a buffered step text * {@link #addStepToBuffer(String, String[][])}. The Mapping will occur in * order of the incoming result. So the first result will be mapped to the * first step in the buffer, the second result to the second step and so on. * For the duration of the step, the amount of time between the last result or * the start of the scenario will be set. * * @param result * the result of the step */ @WebMethod(operationName = "addResultToBufferStep") public void addResultToBufferStep(@WebParam(name = "result") StepResult result) { if (!currentScenario.hasStepAnnotations() || currentScenario.getStepAnnotation(resultPos) == null) { return; } Long endTimestamp = System.currentTimeMillis(); StepAnnotation annotation = currentScenario.getStepAnnotation(resultPos); annotation.setMillisecondsFrom(currentEndTimestamp - this.scenarioStartTimestamp); if (result != null) { annotation.setStepResult(result); } annotation.setDurationMillis(endTimestamp - currentEndTimestamp); this.currentEndTimestamp = endTimestamp; this.resultPos++; } /** * Adds a step with its reported result to the buffer. Uses * {@link #addStepToBuffer(String, String[][])} and * {@link #addResultToBufferStep(StepResult)} * * @param steptext * The gherkin text for the step * @param datatable * optional input data for the step * @param result * the result of the step */ @WebMethod(operationName = "addStepWithResult") public void addStepWithResult(@WebParam(name = "steptext") String steptext, @WebParam(name = "datatable") String[][] datatable, @WebParam(name = "result") StepResult result) { if (currentScenario == null) { return; } StepAnnotation stepAnnot = new StepAnnotation(); stepAnnot.setSteptext(steptext); stepAnnot.setDataTables(datatable); currentScenario.addStepAnnotation(stepAnnot); addResultToBufferStep(result); } /** * Starts the server process with the given arguments. * @param config * 0 - Publishing address for the SOAP-service 1 - Output-Directory where to store the * annotation-files/video-files 2 - Video-with for the capturing area 3 - Video-height for * the capturing area */ public static void main(String[] config) { if (config.length < 4) { throw new IllegalArgumentException("Misconfiguration, parameters to set: " + "<publish_adress> <outputDirectory>, <video_width>, <video_height>"); } final AnnotationService service = new AnnotationService(config[1], config[2], config[3]); final Endpoint endpoint = Endpoint.publish(config[0], service); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { endpoint.stop(); /* * Unfortunately it is not possible to call stopScenario() here as * MonteMediaLibrary will start and endless loop. Probably it is due to * the fact that MonteMediaLibrary will access the currentThread when it * stops the video recording. As a workaround, the Server will be start * in an own JVM and stopScenario() will be called in a shutdown hook * outside from the adapter. */ } }); } }